﻿using System;
using System.Net;
using System.Text;

namespace LitS3
{
    /// <summary>
    /// The base class for all CloudFront requests.
    /// </summary>
    public abstract class CloudFrontRequest<TResponse>
        where TResponse : CloudFrontResponse, new()
    {
        /// <summary>
        /// Gets the service this request will operate against.
        /// </summary>
        public CloudFrontService Service { get; private set; }

        protected HttpWebRequest WebRequest { get; private set; }

        internal CloudFrontRequest(CloudFrontService service, string method, string distributionID, string queryString)
        {
            this.Service = service;
            this.WebRequest = CreateWebRequest(method, distributionID, queryString);
        }

        HttpWebRequest CreateWebRequest(string method, string distributionID, string queryString)
        {
            var uriString = new StringBuilder("https://");

            uriString.Append(Service.Host);
            
            if (Service.CustomPort != 0)
                uriString.Append(':').Append(Service.CustomPort);

            uriString.Append(Service.BasePath);

            if (distributionID != null)
                uriString.Append("/" + distributionID);

            if (queryString != null)
                uriString.Append(queryString);

            var uri = new Uri(uriString.ToString());

            HttpWebRequest request = (HttpWebRequest)System.Net.WebRequest.Create(uri);
            request.Method = method;
            request.AllowWriteStreamBuffering = true; // AddObject will make this false
            request.AllowAutoRedirect = true;

            // CloudFront will never "timeout" a request. However, network delays may still cause a
            // timeout according to WebRequest's ReadWriteTimeout property, which you can modify.
            request.Timeout = int.MaxValue;

            return request;
        }

        #region Expose Amazon-relevant mirrored properties of HttpWebRequest

        /// <summary>
        /// Gets a value that indicates whether a response has been received from Amazon.
        /// </summary>
        public bool HaveResponse
        {
            get { return WebRequest.HaveResponse; }
        }

        /// <summary>
        /// Gets or sets a value that indicates whether to make a persistent connection to Amazon.
        /// </summary>
        public bool KeepAlive
        {
            get { return WebRequest.KeepAlive; }
            set { WebRequest.KeepAlive = value; }
        }

        /// <summary>
        /// Gets or sets proxy information for this request.
        /// </summary>
        public IWebProxy Proxy
        {
            get { return WebRequest.Proxy; }
            set { WebRequest.Proxy = value; }
        }

        /// <summary>
        /// Gets or sets a time-out in milliseconds when writing to or reading from a stream.
        /// The default value is 5 minutes.
        /// </summary>
        public int ReadWriteTimeout
        {
            get { return WebRequest.ReadWriteTimeout; }
            set { WebRequest.ReadWriteTimeout = value; }
        }

        /// <summary>
        /// Gets the service point to use for this request.
        /// </summary>
        public ServicePoint ServicePoint
        {
            get { return WebRequest.ServicePoint; }
        }

        #endregion

        protected void AuthorizeIfNecessary()
        {
            if (!CloudFrontAuthorizer.IsAuthorized(WebRequest)) Authorize();
        }

        protected virtual void Authorize()
        {
            if (CloudFrontAuthorizer.IsAuthorized(WebRequest))
                throw new InvalidOperationException("This request has already been authorized.");

            Service.Authorizer.AuthorizeRequest(WebRequest);
        }

        void TryThrowCloudFrontException(WebException exception)
        {
            // if this is a protocol error and the response type is XML, we can expect that
            // Amazon sent us an <Error> message.
            if (exception.Status == WebExceptionStatus.ProtocolError &&
                exception.Response.ContentType == "application/xml" &&
                exception.Response.ContentLength > 0)
            {
                var wrapped = CloudFrontException.FromWebException(exception);
                throw wrapped; // do this on a separate statement so the debugger can re-execute
            }
        }

        /// <summary>
        /// Gets the CloudFront REST response synchronously.
        /// </summary>
        public virtual TResponse GetResponse()
        {
            AuthorizeIfNecessary();

            try
            {
                return new TResponse { WebResponse = (HttpWebResponse)WebRequest.GetResponse() };
            }
            catch (WebException exception)
            {
                TryThrowCloudFrontException(exception);
                throw;
            }
        }

        /// <summary>
        /// Begins an asynchronous request to Amazon.
        /// </summary>
        public virtual IAsyncResult BeginGetResponse(AsyncCallback callback, object state)
        {
            AuthorizeIfNecessary();
            return WebRequest.BeginGetResponse(callback, state);
        }

        /// <summary>
        /// Ends an asynchronous call to BeginGetResponse().
        /// </summary>
        public virtual TResponse EndGetResponse(IAsyncResult asyncResult)
        {
            try
            {
                return new TResponse { WebResponse = (HttpWebResponse)WebRequest.EndGetResponse(asyncResult) };
            }
            catch (WebException exception)
            {
                TryThrowCloudFrontException(exception);
                throw;
            }
        }

        /// <summary>
        /// Cancels an asynchronous request to Amazon.
        /// </summary>
        public void Abort()
        {
            WebRequest.Abort();
        }
    }
}
